    ; 内核函数
    ; @author: Milon
    ; @date: 2024/04/30

    SHOW_ATTR           equ 0000_0111B   ; 显示属性

    %include "./common/global_defs.h"

    bits 64

_prn_str_locker dq 0


put_string64:                           ; 过程：打印字符串
                                        ; 输入：rbx 字符串地址，0结尾
        push rbx
        push rcx

        pushfq
        cli                             ; 打印字符串过程中屏蔽中断，避免输出乱序字符
        SET_SPIN_LOCK rcx, qword [rel _prn_str_locker]  ; 加锁
    .next_char:
        mov cl, [rbx]
        cmp cl, 0
        jz .exit                        ; 如果当前字符是0，结束过程
        
        call put_char                   ; 否则调用put_char打印该字符
        inc rbx                         ; bx指向下一个字符
        jmp .next_char

    .exit:
        mov qword [rel _prn_str_locker], 0  ; 释放锁
        popfq
        pop rcx
        pop rbx

        ret


put_char:                               ; 过程：打印单个字符
                                        ; 输入：cl = 单个字符的ASCII码
        push rax
        push rbx
        push rcx
        push rdx
        push rsi
        push rdi
        push r9

        mov r9, UPPER_TEXT_VIDEO

        call get_cursor                 ; 读取光标位置到ax中

        cmp cl, 0x0d                    ; 判断要打印的字符是否回车\r
        jz .print_CR

        cmp cl, 0x0a                    ; 判断要打印的字符是否换行\n
        jz .print_LF

        ; 打印其它字符
        jmp .put_other

    ; 打印回车字符，使光标移至行首
    .print_CR:
        ; 当前光标位置除以80得到行号在al中
        mov bl, 80
        div bl
        ; 上一步计算得出的行号×80便是当前行行首，结果在ax中
        mul bl
        mov bx, ax
        jmp .setcur


    ; 打印换行符，将光标移至下一行的当前位置
    .print_LF:
        mov bx, ax                      ; 将ax中的结果临时保存至bx
        add bx, 80                      ; 将当前光标位置+80就是下一行当前位置
        jmp .roll_screen

    ; 打印其它字符
    .put_other:
        ; 使ds指向显存
        movzx rbx, ax
        shl bx, 1                       ; 将光标位置左移1位，相当于×2，得到显存中的字符偏移位置
        mov [rbx + r9], cl                    ; 将cl中的ASCII码写入显存
        mov byte [rbx + r9 + 1], SHOW_ATTR    ; 显示属性
        
        ; 将光标位置还原并+1
        shr bx, 1
        inc bx
        
    ; 滚屏
    .roll_screen:
        cmp bx, 2000
        jl .setcur                      ; 如果当前光标位置 < 2000，则不需要滚屏，直接设置光标位置

        ; 滚屏操作
        ; 将ds:si指向屏幕第1行开始位置，di指向屏幕第0行开始位置

        lea rsi, [r9 + 0xa0]
        mov rdi, r9
        mov rcx, 1920 / 4               ; 传送1920/4个四字
        cld                             ; 清方向标志，表示正向传送
        rep movsq

        ; 清除屏幕最底下一行
        mov rcx, 80
    .clear_last:
        mov byte [rdi], ' '              ; 使用空格填充
        mov byte [rdi + 1], SHOW_ATTR    ; 显示属性，绿底白字
        add rdi, 2
        loop .clear_last

        sub rbx, 80                      ; 滚屏后，重新将光标位置设置在最后一行的相应位置

    .setcur:
        call set_cursor

        pop r9
        pop rdi
        pop rsi
        pop rdx
        pop rcx
        pop rbx
        pop rax

        ret


get_cursor:                             ; 过程：获取光标位置
                                        ; 返回：ax = 光标位置
    push dx

    ; 通过索引寄存器使数据寄存器指向光标寄存器高8位
    mov dx, 0x3d4
    mov al, 0x0e
    out dx, al

    ; 通过数据端口获取光标寄存器高8位
    mov dx, 0x3d5
    in al, dx
    mov ah, al

    ; 通过索引寄存器使数据寄存器指向光标寄存器低8位
    mov dx, 0x3d4
    mov al, 0x0f
    out dx, al

    ; 将光标寄存器低8位读入al中
    mov dx, 0x3d5
    in al, dx                           ; 此时ax中有了完整的光标位置（高8位和低8位）

    pop dx
    ret


set_cursor:                             ; 过程：设置光标位置
                                        ; 输入：bx = 光标位置
    push rax
    push rdx
    
    ; 通过索引寄存器将数据寄存器指向光标高8位
    mov dx, 0x3d4
    mov al, 0x0e
    out dx, al

    ; 通过数据寄存器写入光标高8位
    mov dx, 0x3d5
    mov al, bh
    out dx, al

    ; 通过索引寄存器将数据寄存器指向光标低8位
    mov dx, 0x3d4
    mov al, 0x0f
    out dx, al

    ; 通过数据寄存器写入光标高8位
    mov dx, 0x3d5
    mov al, bl
    out dx, al

    pop rdx
    pop rax

    ret


_prnxy_locker   dq  0

put_cstringxy64:                        ; 过程：在指定位置用指定颜色显示0终止的字符串（只适用于打印图形字符）
                                        ; 输入：RBX=字符串首地址
                                        ;       DH=行，DL=列
                                        ;       R9B=颜色属性
        push rax
        push rbx
        push rcx
        push rdx
        push r8

        ; 计算指定位置在显存中的偏移量
        mov al, dh
        mov ch, 160
        mul ch
        shl dl, 1
        and dx, 0x00ff
        add ax, dx
        and rax, 0x000000000000ffff
        
        pushfq
        cli
        SET_SPIN_LOCK r8, qword [rel _prnxy_locker] ; 打印过程需要加锁，避免多线程交错打印

        mov r8, UPPER_TEXT_VIDEO
    .nextc:
        mov dl, [rbx]
        or dl, dl
        jz .exit
        mov [rax + r8], dl
        mov [rax + r8 + 1], r9b
        inc rbx
        add rax, 2
        jmp .nextc

    .exit:
        mov qword [rel _prnxy_locker], 0    ; 释放锁
        popfq
        pop r8
        pop rdx
        pop rcx
        pop rbx
        pop rax
        ret


make_interrupt_gate:                    ; 过程：创建64位的中断门
                                        ; 输入：RAX=例程的线性地址
                                        ; 输出：RDI:RSI=中断门

    ; 构建中断门的位32~63
    mov rdi, rax
    shr rdi, 32

    push rax
    mov [rsp + 4], eax
    mov word [rsp + 2], CORE_CODE64_SEL
    mov word [rsp + 4], 1000_1110_00000000B ; P=1, DPL=00, TYPE=1110
    pop rsi

    ret


mount_idt_entry:                        ; 过程：在中断描述符表IDT中安装门描述符
                                        ; 输入：R8=中断向量号
                                        ;      RDI:RSI=门描述符
    push r8
    push r9

    shl r8, 4                           ; 相当于×16，中断号得到在IDT中的偏移地址
    mov r9, UPPER_IDT_LINEAR
    mov [r9 + r8], rsi
    mov [r9 + r8 + 8], rdi

    pop r9
    pop r8

    ret


_core_next_linear  dq CORE_ALLOC_START
_core_alloc_locker dq 0

core_memory_allocate:                   ; 过程：在虚拟地址空间的高端（内核）分配内存
                                        ; 输入：RCX=请求分配的字节数
                                        ; 输出：R13=本次分配的起始线性地址
                                        ;       R14=下次分配的起始线性地址

        pushfq
        cli
        SET_SPIN_LOCK r14, qword [rel _core_alloc_locker]   ; 防止多个线程同时基于同一个线性地址分配内存，需要加锁

        mov r13, [rel _core_next_linear]    ; 本次分配内存的起始线性地址
        lea r14, [r13 + rcx]
        test r14, 0x07                      ; 测试是否8字节对齐（即低3位是否全为0）
        jz .aligned

        ; 如果不是8字节对齐，则强制对齐
        add r14, 8
        shr r14, 3
        shl r14, 3

    .aligned:
        mov [rel _core_next_linear], r14    ; 将下次分配内存线性地址写回
        
        ; 释放锁
        popfq
        mov qword [rel _core_alloc_locker], 0

        push r13
        push r14

        ; 为本次内存申请分配页
        ; 清除掉页内偏移部分（页按4K字节对齐）
        shr r13, 12
        shl r13, 12
        shr r14, 12
        shl r14, 12
    .append_page:
        call setup_paging_for_laddr
        add r13, 0x1000
        cmp r13, r14
        jle .append_page

        pop r14
        pop r13

        ret


user_memory_allocate:                   ; 在用户任务的私有空间（低端）分配内存
                                        ; 输入：R11=任务控制块PCB的线性地址
                                        ;       RCX=希望分配的字节数
                                        ; 输出：R13=本次分配的起始线性地址
                                        ;       R14=下次分配的起始线性地址

        mov r13, [r11 + 24]             ; 本次内存分配的起始地址 -> r13
        lea r14, [r13 + rcx]    

        ; 下次分配地址，8字节对齐
        test r14, 0x07
        jz .aligned
        add r14, 8
        shr r14, 3
        shl r14, 3

    .aligned:
        mov [r11 + 24], r14

        push r13
        push r14

        shr r13, 12
        shl r13, 12
        shr r14, 12
        shl r14, 12
    .next:
        call setup_paging_for_laddr
        add r13, 0x1000
        cmp r13, r14
        jle .next

        pop r14
        pop r13

        ret


_spaging_locker dq  0

setup_paging_for_laddr:                 ; 过程：为指定的线性地址安装分页系统（表项）
                                        ; 输入：R13=线性地址
        push r14
        push rax
        push rcx

        pushfq
        cli
        SET_SPIN_LOCK r14, qword [rel _spaging_locker]

        call lin_to_lin_of_pml4e        ; 得到线性地址对应的四级头表项线性地址
        test qword [r14], 1             ; 测试P位是否为1
        jnz .b0                         ; P=1则跳转

        ; P=0则创建对应的页目录指针表
        call allocate_a_4k_page
        or rax, 0x07                    ; US=RW=P=1
        mov [r14], rax                  ; 将四级头表项写回四级头表

        ; 清空刚创建的页目录指针表
        call lin_to_lin_of_pdpte
        shr r14, 12
        shl r14, 12
        mov rcx, 512
    .cls0:
        mov qword [r14], 0
        add r14, 8
        loop .cls0

    .b0:
        call lin_to_lin_of_pdpte         ; 获取页目录指针项线性地址
        test qword [r14], 1              ; 测试页目录指针项是否存在
        jnz .b1

        ; 如果不存在，则创建对应的页目录表
        call allocate_a_4k_page
        or rax, 0x07
        mov [r14], rax                   ; 写回页目录指针项

        ; 清空刚创建的页目录表
        call lin_to_lin_of_pdte
        shr r14, 12
        shl r14, 12
        mov rcx, 512
    .cls1:
        mov qword [r14], 0
        add r14, 8
        loop .cls1

    .b1:
        ; 检查该线性地址对应的页目录项是否存在
        call lin_to_lin_of_pdte
        test qword [r14], 1
        jnz .b2

        ; 如果不存在，则创建对应的页表
        call allocate_a_4k_page
        or rax, 0x07
        mov [r14], rax

        ; 清空刚刚创建的页表
        call lin_to_lin_of_pte
        shr r14, 12
        shl r14, 12
        mov rcx, 512
    .cls2:
        mov qword [r14], 0
        add r14, 8
        loop .cls2

    .b2:
        ; 检查该线性地址对应的页表项是否存在
        call lin_to_lin_of_pte
        test qword [r14], 1
        jnz .b3

        ; 创建线性地址对应的物理页
        call allocate_a_4k_page
        or rax, 0x07
        mov [r14], rax
        
    .b3:
        mov qword [rel _spaging_locker], 0
        popfq

        pop rcx
        pop rax
        pop r14
        ret


; 页映射位串
_page_bit_map   times (2048 / 4 / 8)                       db 0xff    ; 物理内存低端2MB，已被内核占用
                times (PHY_MEMORY_SIZE - 2) * 1024 / 4 / 8 db 0x00
_page_map_len   equ $ - _page_bit_map


allocate_a_4k_page:                         ; 过程：分配一个4KB的页
                                            ; 输入：无
                                            ; 输出：RAX=页的物理地址

        xor rax, rax
    .b1:
        lock bts [rel _page_bit_map], rax   ; 将对应位挤到CF
        jnc .b2                             ; 如果CF=0，说明对应位是0，可将该页用于分配，跳转
        inc rax
        cmp rax, _page_map_len * 8
        jl .b1

    .b2:
        shl rax, 12
        ret


lin_to_lin_of_pml4e:                    ; 过程：返回线性地址四级头表项的线性地址
                                        ; 输入：R13=线性地址
                                        ; 输出：R14=对应的4级头表项的线性地址
    push r13

    mov r14, 0x0000_ff80_0000_0000
    and r13, r14                        ; 保留r13的48为线性地址中的最高9bit
    shr r13, 36                         ; 该9bit右移36位，得到 页表索引 × 8，也就是页表内偏移量
    mov r14, 0xffff_ffff_ffff_f000
    add r14, r13                        ; 四级头表项线性地址+页表内偏移量，得到入参线性地址对应四级头表项的线性地址

    pop r13
    ret


lin_to_lin_of_pdpte:                    ; 过程：返回指定的线性地址所对应的页目录指针项的线性地址
                                        ; 输入：R13=线性地址
                                        ; 输出：R14=对应的页目录指针项的线性地址
    push r13

    mov r14, 0x0000_ffff_c000_0000      
    and r13, r14                        ; 保留48位线性地址的 四级头表索引 + 页目录指针表索引
    shr r13, 27                         ; 原四级头表索引变为页表索引，原页目录指针表索引变为页内偏移
    mov r14, 0xffff_ffff_ffe0_0000      ; 页目录指针表地址前缀
    add r14, r13                        

    pop r13
    ret


lin_to_lin_of_pdte:                     ; 过程：返回指定的线性地址所对应的页目录项的线性地址
                                        ; 输入：R13=线性地址
                                        ; 输出：R14=对应的页目录项的线性地址

    push r13

    mov r14, 0x0000_ffff_ffe0_0000      
    and r13, r14                        
    shr r13, 18                         
    mov r14, 0xffff_ffff_c000_0000      
    add r14, r13                        

    pop r13
    ret


lin_to_lin_of_pte:                          ; 过程：返回指定的线性地址所对应的页表项的线性地址
                                            ; 输入：R13=线性地址
                                            ; 输出：R14=对应的页表项的线性地址
    push r13

    mov r14, 0x0000_ffff_ffff_f000      
    and r13, r14                        
    shr r13, 9                         
    mov r14, 0xffff_ff80_0000_0000      
    add r14, r13                        

    pop r13
    ret



make_tss_descriptor:                        ; 过程：创建64位的TSS描述符
                                            ; 输入：RAX=TSS的线性地址
                                            ; 输出：RDI:RSI=TSS描述符
    push rax

    mov rdi, rax
    shr rdi, 32

    push rax
    shl qword [rsp], 16                     ; 线性基地址23~0移位到正确位置
    mov word [rsp], 104                     ; TSS段界限
    mov al, [rsp + 5]                       ; 取TSS线性基地址位31~24
    mov [rsp + 7], al                       ; TSS线性基地址位31~24放到正确位置
    mov byte [rsp + 5], 0x89                ; P=1, DPL=00, TYPE=1001
    mov byte [rsp + 6], 0x00                ; G=0, 段界限位19~16=0
    pop rsi

    pop rax
    ret


_copy_locker    dq  0

copy_current_pml4:                          ; 创建新的4级头表，并复制当前4级头表的内容
                                            ; 输入：无
                                            ; 输出：RAX=新4级头表的物理地址及属性
    push rcx
    push r13
    push rsi
    push rdi

    pushfq
    cli
    SET_SPIN_LOCK rcx, qword [rel _copy_locker]

    call allocate_a_4k_page                 ; 分配一个物理页，用于新任务的四级头表
    or rax, 0x07
    mov r13, NEW_PML4_LINEAR
    call mapping_laddr_to_page              ; 新四级头表线性地址映射到刚分配的物理页
    invlpg [r13]                            ; 使TLB相关表项失效

    ; 将活动四级头表的内容复制到新任务的四级头表
    mov rsi, 0xffff_ffff_ffff_f000
    mov rdi, r13
    mov rcx, 512
    cld
    rep movsq

    ; 将新任务的四级头表最后一项指向它自己
    mov [r13 + 0xff8], rax
    invlpg [r13 + 0xff8]

    mov qword [rel _copy_locker], 0
    popfq

    pop rdi
    pop rsi
    pop r13
    pop rcx
    ret


_mapping_locker dq  0

mapping_laddr_to_page:                      ; 为指定的线性地址安装指定的物理页
                                            ; 输入：R13=线性地址
                                            ;      RAX=页的物理地址（含属性）
        push r14
        push rcx

        pushfq
        cli
        SET_SPIN_LOCK r14, qword [rel _mapping_locker]

        push rax

        call lin_to_lin_of_pml4e            ; 得到线性地址对应的四级头表项线性地址
        test qword [r14], 1                 ; 测试P位是否为1
        jnz .b0                             ; P=1则跳转

        ; P=0则创建对应的页目录指针表
        call allocate_a_4k_page
        or rax, 0x07                        ; US=RW=P=1
        mov [r14], rax                      ; 将四级头表项写回四级头表

        ; 清空刚创建的页目录指针表
        call lin_to_lin_of_pdpte
        shr r14, 12
        shl r14, 12
        mov rcx, 512
    .cls0:
        mov qword [r14], 0
        add r14, 8
        loop .cls0

    .b0:
        call lin_to_lin_of_pdpte            ; 获取页目录指针项线性地址
        test qword [r14], 1                 ; 测试页目录指针项是否存在
        jnz .b1

        ; 如果不存在，则创建对应的页目录表
        call allocate_a_4k_page
        or rax, 0x07
        mov [r14], rax                      ; 写回页目录指针项

        ; 清空刚创建的页目录表
        call lin_to_lin_of_pdte
        shr r14, 12
        shl r14, 12
        mov rcx, 512
    .cls1:
        mov qword [r14], 0
        add r14, 8
        loop .cls1

    .b1:
        ; 检查该线性地址对应的页目录项是否存在
        call lin_to_lin_of_pdte
        test qword [r14], 1
        jnz .b2

        ; 如果不存在，则创建对应的页表
        call allocate_a_4k_page
        or rax, 0x07
        mov [r14], rax

        ; 清空刚刚创建的页表
        call lin_to_lin_of_pte
        shr r14, 12
        shl r14, 12
        mov rcx, 512
    .cls2:
        mov qword [r14], 0
        add r14, 8
        loop .cls2

    .b2:
        ; 获取页表项的线性地址
        call lin_to_lin_of_pte
        pop rax
        mov [r14], rax
        
        mov qword [rel _mapping_locker], 0
        popfq

        pop rcx
        pop r14
        ret


_read_hdd_locker    dq  0

read_hard_disk_0:                           ; 从硬盘读取一个逻辑扇区
                                            ; RAX=逻辑扇区号
                                            ; RBX=目标缓冲区线性地址
                                            ; 返回：RBX=RBX+512
        push rax
        push rcx
        push rdx

        pushfq
        cli
        SET_SPIN_LOCK rdx, qword [rel _read_hdd_locker]

        push rax

        ; 设置读取扇区数=1
        mov dx, 0x01f2
        mov al, 0x01
        out dx, al

        pop rax
        
        ; 设置起始扇区号
        inc dx      ; 0x01f3
        out dx, al  ; LBA 0~7

        inc dx      ; 0x01f4
        mov al, ah
        out dx, al  ; LBA 8~15

        inc dx      ; 0x01f5
        shr rax, 16
        out dx, al  ; LBA 16~23

        inc dx      ; 0x01f6
        mov al, ah
                            ; 位5位7固定是1，位6=1代表LBA模式，位4=0代表主硬盘，低4位暂设为0
        or al, 1110_0000B   ; 此时al低4位保存了LBA28的最高4位
        out dx, al

        inc dx          ; 0x01f7
        mov al, 0x20    ;读指令
        out dx, al

        ; 判断硬盘是否可读
    .waits:
        in al, dx
        and al, 1000_1000B
        cmp al, 0000_1000B
        jnz .waits

        ; 硬盘准备好数据后，开始读入内存
        mov rcx, 256
        mov dx, 0x01f0
    .readw:
        in ax, dx
        mov [rbx], ax
        add rbx, 2
        loop .readw

        mov qword [rel _read_hdd_locker], 0
        popfq

        pop rdx 
        pop rcx 
        pop rax

        ret


  _process_id   dq    0

generate_process_id:                        ; 生成唯一的进程标识
                                            ; 返回：RAX=进程标识

    mov rax, 1
    lock xadd qword [rel _process_id], rax

    ret


  _thread_id   dq    0

generate_thread_id:                         ; 生成唯一的线程标识
                                            ; 返回：RAX=线程标识

    mov rax, 1
    lock xadd qword [rel _thread_id], rax

    ret



  _screen_row   db  8

get_screen_row:                             ; 返回下一个屏幕坐标行的行号
                                            ; 返回：DH=行号
    mov dh, 1
    lock xadd byte [rel _screen_row], dh

    ret


_cmos_locker    dq  0

get_cmos_time:                              ; 从CMOS中获取当前时间
                                            ; 输入：RBX=缓冲区线性地址
        push rax

        pushfq
        cli
        SET_SPIN_LOCK rax, qword [rel _cmos_locker]

        ; 读寄存器A，获取最高位UIP判断是否处在更新周期，同时阻断NMI
        mov al, 0x0a
        or al, 1000_0000B
        out 0x70, al

    .w0:
        in al, 0x71
        test al, 1000_0000B                 ; 测试 UIP 位
        jnz .w0                             ; 如果不等于0则重试

        ; 读取时
        mov al, 0x04
        or al, 1000_0000B
        out 0x70, al
        in al, 0x71
        mov ah, al

        ; 取BCD编码高4位
        shr ah, 4
        and ah, 0x0f
        add ah, 0x30                        ; + '0'转为ASCII字符
        mov [rbx], ah

        ; 取BCD编码低4位
        and al, 0x0f
        add al, 0x30
        mov [rbx + 1], al

        mov byte [rbx + 2], ':'

        ; 读取分
        mov al, 0x82
        out 0x70, al
        in al, 0x71                         ; 读RTC当前时间(分)
        mov ah, al

        ; 取BCD编码高4位
        shr ah, 4
        and ah, 0x0f
        add ah, 0x30                        ; + '0'转为ASCII字符
        mov [rbx + 3], ah

        ; 取BCD编码低4位
        and al, 0x0f
        add al, 0x30
        mov [rbx + 4], al

        mov byte [rbx + 5], ':'

        ; 读取秒
        xor al, al
        or al, 1000_0000B
        out 0x70, al
        in al, 0x71
        mov ah, al

        ; 取BCD编码高4位
        shr ah, 4
        and ah, 0x0f
        add ah, 0x30                        ;  + '0'转为ASCII字符
        mov [rbx + 6], ah

        ; 取BCD编码低4位
        and al, 0x0f
        add al, 0x30
        mov [rbx + 7], al

        mov byte [rbx + 8], 0               ; 空字符结尾

        mov qword [rel _cmos_locker], 0
        popfq

        pop rax
        ret


get_cpu_number:                             ; 获取当前处理器的编号
                                            ; 返回：RAX=处理器编号
    pushfq
    cli

    swapgs
    mov rax, [gs:16]
    swapgs

    popfq
    ret


memory_allocate:                            ; 用户空间的内存分配
                                            ; 进入：RDX=期望分配的字节数
                                            ; 输出：R13=所分配内存的起始线性地址
    push rcx
    push r11
    push r14
    
    pushfq
    cli
    swapgs
    mov r11, [gs:8]                         ; 当前任务PCB线性地址 -> r11
    swapgs
    popfq

    mov rcx, rdx
    call user_memory_allocate

    pop r14
    pop r11
    pop rcx
    ret

